Parallel Programming under the .NET Platform

If you go shopping at any electronic “super store,” you will quickly notice that computers which support two or more CPUs (aka, cores) are commonplace. Not only are they commonplace, they are quite cost effective; dual core laptops can be purchased for less than $500.00 USD. When a machine supports multiple CPUs, it has the ability to execute threads in a parallel fashion, which can significantly improve the runtime performance of applications.

Traditionally speaking, if you wanted to build a .NET application which can distribute its workload across multiple cores, you needed to be quite skilled in multithreaded programming techniques (using many of the topics seen in this chapter). While this was certainly possible, doing so was tedious and error prone, given the inherit complexities of building multithreaded applications.

With the release of .NET 4.0, you are provided with a brand new parallel programming library. Using the types of System.Threading.Tasks, you can build fine-grained, scalable parallel code without having to work directly with threads or the thread pool. Furthermore, when you do so, you can make use of strongly typed LINQ queries (via parallel LINQ, or PLINQ) to divide up your workload.

The Task Parallel Library API

Collectively speaking, the types of System.Threading.Tasks (as well as some related types in System.Threading) are referred to as the Task Parallel Library, or TPL. The TPL will automatically distribute your application’s workload across available CPUs dynamically using the CLR thread pool. The TPL handles the partitioning of the work, thread scheduling, state management, and other low-level details. The end result is that you can maximize the performance of your .NET applications, while being shielded from many of complexities of directly working with threads. Figure 19-3 shows the members of this new .NET 4.0 namespace.

Figure 19-3

Figure 19-3 Members of the System.Threading.Tasks namespace

Starting with .NET 4.0, use of the TPL is the recommended way to build multithreaded applications. This is certainly not to suggest that having an understanding of traditional multithreading techniques using asynchronous delegates or the classes of System.Threading is somehow obsolete. In fact, to effectively use the TPL, you must understand primitive such as threads, locks, and concurrency. Furthermore, many situations that require multiple threads (such as asynchronous calls to remote objects) can be handled without the use of the TPL. Nevertheless, the amount of time you will need to directly work with the Thread class decreases significantly.

Finally, be aware that just because you can, does not mean you should. In the same way that creating multiple threads can slow down the execution of your .NET programs, authoring a ton of unnecessary parallel tasks can hurt performance. Use the functionality of the TPL only when you have a workload which truly creates a bottleneck in your programs, such as iterating over hundreds of objects, processing data in multiple files, etc.

Note The TPL infrastructure is rather intelligent. If it determines that a set of tasks would gain little or no benefit by running in parallel, it will opt to perform them in sequence.

The Role of the Parallel Class

The primary class of the TPL is System.Threading.Tasks.Parallel. This class supports a number of methods which allow you to iterate over a collection of data (specifically, an object implementing IEnumerable<T>) in a parallel fashion. If you were to look up the Parallel class in the .NET Framework 4.0 SDK documentation, you’ll see that this class supports two primary static methods, Parallel.For() and Parallel.ForEach(), each of which defines numerous overloaded versions.

These methods allow you to author a body of code statements which will be processed in a parallel manner. In concept, these statements are the same sort of logic you would write in a normal looping construct (via the for or foreach C# keywords). The benefit however, is that the Parallel class will pluck threads from the thread pool (and manage concurrency) on your behalf.

Both of these methods require you to specify an IEnumerable or IEnumerable<T> compatible container that holds the data you need to process in a parallel manner. The container could be a simple array, a non-generic collection (such as ArrayList), a generic collection (such as List<T>), or the results of a LINQ query.

In addition, you will need to make use of the System.Func<T> and System.Action<T> delegates to specify the target method which will be called to process the data. You’ve already encountered the Func<T> delegate in Chapter 13 during your investigation of LINQ to Objects. Recall that Func<T> represents a method which can have a given return value and a varied number of arguments. The Action<T> delegate is very similar to Func<T>, in that it allows you to point to a method taking some number of parameters. However, Action<T> specifies a method which can only return void.

While you could call the Parallel.For() and Parallel.ForEach() methods and pass a strongly typed Func<T> or Action<T> delegate object, you can simplify your programming by making use of a fitting C# anonymous method or lambda expression.

Understanding Data Parallelism

The first way to use the TPL is to perform data parallelism. Simply put, this term refers to the task of iterating over an array or collection in a parallel manner using the Parallel.For() or Parallel.ForEach() methods. Assume you need to perform some labor intensive File IO operations. Specifically, you need to load a large number of *.jpg files into memory, flip them upside-down, and save the modified image data to a new location.

The .NET Framework 4.0 SDK documentation provides a console based example of this very thing, however we will perform the same overall task using a graphical user interface. To illustrate, create a Windows Forms application named DataParallelismWithForEach, and rename the Form1.cs to MainForm.cs. Once you do, import the following namespaces in your primary code file:

// Need these namespaces!
using System.Threading.Tasks;
using System.Threading;
using System.IO;

The GUI of the application consists of a multiline TextBox and a single Button (named btnProcessImages). The purpose of the text area is to allow you to enter data while the work is being performed in the background, thus illustrating the non-blocking nature of the parallel task. The Click event of this Button will eventually make use of the TPL, but for now, author the following blocking code:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    private void btnProcessImages_Click(object sender, EventArgs e)
    {
        ProcessFiles();
    }

    private void ProcessFiles()
    {
        // Load up all *.jpg files, and make a new folder for the modified data.
        string[] files = Directory.GetFiles
            (@"C:\Users\AndrewTroelsen\Pictures\My Family", "*.jpg",
            SearchOption.AllDirectories);
        string newDir = @"C:\ModifiedPictures";
        Directory.CreateDirectory(newDir);

        // Process the image data in a blocking manner.
        foreach (string currentFile in files)
        {
            string filename = Path.GetFileName(currentFile);

            using (Bitmap bitmap = new Bitmap(currentFile))
            {
                bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
                bitmap.Save(Path.Combine(newDir, filename));
                this.Text = string.Format("Processing {0} on thread {1}", filename,
                Thread.CurrentThread.ManagedThreadId);
            }
        }
        this.Text = "All done!";
    }
}

Notice that the ProcessFiles() method will rotate each *.jpg file under my personal Pictures\My Family subdirectory, which currently contains a total of 37 files (be sure to update the path sent into Directory.GetFiles() as necessary). Currently, all of the work is happening on the primary thread of the executable. Therefore, if the button is clicked, the program will appear to hang. Furthermore, the caption of the window will also report that the same primary thread is processing the file (Figure 19- 4).

Figure 19-4

Figure 19-4 Currently, all action is taking place on the primary thread

To process the files on as many CPUs as possible, you can rewrite the current foreach loop to make use of Parallel.ForEach(). Recall that this method has been overloaded numerous times, however in the simplest form, you must specify the IEnumerable<T> compatible object that contains the items to process (that would be the files string array) and an Action<T> delegate which points to the method that will perform the work. Here is the relevant update, using the C# lambda operator, in place of a literal Action<T> delegate object:

// Process the image data in a parallel manner!
Parallel.ForEach(files, currentFile =>
    {
        string filename = Path.GetFileName(currentFile);

        using (Bitmap bitmap = new Bitmap(currentFile))
        {
            bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
            bitmap.Save(Path.Combine(newDir, filename));
            this.Text = string.Format("Processing {0} on thread {1}", filename,
                Thread.CurrentThread.ManagedThreadId);
        }
    }
);

Now, if you run program, the TPL will indeed distribute the workload to multiple threads from the thread pool, using as many CPUs as possible. However, you will not see the window’s caption display the name of each unique thread and you won’t see anything if you type in the text box, until all the images have been processed! The reason is that the primary UI thread is still blocked, waiting for all of the other threads to finish up their business.

The Task Class

To keep the user interface responsive, you could certainly make use of asynchronous delegates or the members of the System.Threading namespace directly, but the System.Threading.Tasks namespace provides a simpler alternative, via the Task class. Task allows you to easily invoke a method on a secondary thread, and can be used as a simple alternative to working with asynchronous delegates. Update the Click handler of your Button control as so:

private void btnProcessImages_Click(object sender, EventArgs e)
{
    // Start a new "task" to process the files.
    Task.Factory.StartNew(() =>
    {
        ProcessFiles();
    });
}

The Factory property of Task returns a TaskFactory object. When you call its StartNew() method, you pass in an Action<T> delegate (here, hidden away with a fitting lambda expression) which points to the method to invoke in an asynchronous manner. With this small update, you will now find that the window’s title will show which thread from the thread pool is processing a given file, and better yet, the text area is able to receive input, as the UI thread is no longer blocked.

Handling Cancelation Request

One improvement you can make to the current example is to provide a way for the user to stop the processing of the image data, via a second (aptly named) Cancel button. Thankfully, the Parallel.For() and Parallel.ForEach() methods both support cancellation through the use of cancellation tokens. When you invoke methods on Parallel, you can pass in a ParallelOptions object, which in turn contains a CancellationTokenSource object.

First of all, define a new private member variable in your Form derived class of type CancellationTokenSource named cancelToken:

public partial class MainForm : Form
{
    // New Form level variable.
    private CancellationTokenSource cancelToken =
        new CancellationTokenSource();
...
}

Now, assuming you have added a new Button (named btnCancel) on your designer, handle the Click event, and implement the handler as so:

private void btnCancelTask_Click(object sender, EventArgs e)
{
    // This will be used to tell all the worker threads to stop!
    cancelToken.Cancel();
}

Now, the real modifications need to occur within the ProcessFiles() method. Consider the final implementation:

private void ProcessFiles()
{
    // Use ParallelOptions instance to store the CancellationToken
    ParallelOptions parOpts = new ParallelOptions();
    parOpts.CancellationToken = cancelToken.Token;
    parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

    // Load up all *.jpg files, and make a new folder for the modified data.
    string[] files = Directory.GetFiles
        (@"C:\Users\AndrewTroelsen\Pictures\My Family", "*.jpg",
            SearchOption.AllDirectories);
    string newDir = @"C:\ModifiedPictures";
    Directory.CreateDirectory(newDir);
    
    try
    {
        // Process the image data in a parallel manner!
        Parallel.ForEach(files, parOpts, currentFile =>
        {
            parOpts.CancellationToken.ThrowIfCancellationRequested();
    
            string filename = Path.GetFileName(currentFile);
            using (Bitmap bitmap = new Bitmap(currentFile))
            {
                bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
                bitmap.Save(Path.Combine(newDir, filename));
                this.Text = string.Format("Processing {0} on thread {1}", filename,
                    Thread.CurrentThread.ManagedThreadId);
            }
        }
        );
    this.Text = "All done!";
    }
    catch (OperationCanceledException ex)
    {
        this.Text = ex.Message;
    }
}

Notice that you begin the method by configuring a ParallelOptions object, setting the CancellationToken property to use the CancellationTokenSource token. Also note that when you call the Parallel.ForEach() method, you pass in the ParallelOptions object as the second parameter.

Within the scope of the looping logic, you make a call to ThrowIfCancellationRequested() on the token, which will ensure if the user clicks the Cancel button, all threads will stop and you will be notified via a runtime exception. When you catch the OperationCanceledException error, you will set the text of the main window to the error message.

Understanding Task Parallelism

In addition to data parallelism, the TPL can also be used to easily fire off any number of asynchronous tasks using the Parallel.Invoke() method. This approach is a bit more straightforward than using delegates or members from System.Threading, however if you require more control over the way tasks are executed, you could forgo use of Parallel.Invoke() and make use of the Task class directly, as you did in the previous example.

To illustrate task parallelism, create a new Windows Forms application called MyEBookReader and import the System.Threading.Tasks and System.Net namespaces.

This application is a modification of a useful example in the .NET Framework 4.0 SDK documentation, which will fetch a free e-book from Project Gutenberg (http://www.gutenberg.org), and then perform a set of lengthy tasks in parallel.

The GUI consists of a multi-line TextBox control (named txtBook) and two Button controls (btnDownload and btnGetStats). Once you have designed the UI, handle the Click event for each Button, and in the form’s code file, declare a class level string variable named theEBook. Implement the Click hander for the btnDownload as so:

private void btnDownload_Click(object sender, EventArgs e)
{
    WebClient wc = new WebClient();
    wc.DownloadStringCompleted += (s, eArgs) =>
    {
        theEBook = eArgs.Result;
        txtBook.Text = theEBook;
    };

    // The Project Gutenberg EBook of A Tale of Two Cities, by Charles Dickens
    wc.DownloadStringAsync(new Uri("http://www.gutenberg.org/files/98/98-8.txt"));
}

The WebClient class is a member of System.Net. This class provides a number of methods for sending data to and receiving data from a resource identified by a URI. As it turns out, many of these methods have an asynchronous version, such as DownloadStringAsyn(). This method will spin up a new thread from the CLR thread pool automatically. When the WebClient is done obtaining the data, it will fire the DownloadStringCompleted event, which you are handling here using a C# lambda expression. If you were to call the synchronous version of this method (DownloadString()) the form would appear unresponsive for quite some time.

The Click event hander for the btnGetStats Button control is implemented to extract out the individual words contained in theEBook variable, and then pass the string array to a few helper functions for processing:

private void btnGetStats_Click(object sender, EventArgs e)
{
    // Get the words from the e-book.
    string[] words = theEBook.Split(new char[]
    { ' ', '\u000A', ',', '.', ';', ':', '-', '?', '/' },
    StringSplitOptions.RemoveEmptyEntries);

    // Now, find the ten most common words.
    string[] tenMostCommon = FindTenMostCommon(words);

    // Get the longest word.
    string longestWord = FindLongestWord(words);

    // Now that all tasks are complete, build a string to show all
    // stats in a message box.
    StringBuilder bookStats = new StringBuilder("Ten Most Common Words are:\n");
    foreach (string s in tenMostCommon)
    {
        bookStats.AppendLine(s);
    }
    bookStats.AppendFormat("Longest word is: {0}", longestWord);
    bookStats.AppendLine();
    MessageBox.Show(bookStats.ToString(), "Book info");
}

The FindTenMostCommon() method uses a LINQ query to obtain a list of string objects which occur most often in the string array, while FindLongestWord() locates, well, the longest word:

private string[] FindTenMostCommon(string[] words)
{
    var frequencyOrder = from word in words
        where word.Length > 6
        group word by word into g
        orderby g.Count() descending
        select g.Key;
    string[] commonWords = (frequencyOrder.Take(10)).ToArray();
    return commonWords;
}

private string FindLongestWord(string[] words)
{
    return (from w in words orderby w.Length descending select w).First();
}

If you were to run this project, the amount of time to perform all tasks could take a goodly amount of time, based on the CPU count of your machine and overall processor speed. Eventually, you should see the following output (Figure 19-5).

Figure 19-5

Figure 19-5 Stats about the downloaded EBook

You can help ensure that your application makes use of all available CPUs on the host machine by invoking the FindTenMostCommon() and FindLongestWord()method in parallel. To do so, modify your btnGetStats_Click() method as so:

private void btnGetStats_Click(object sender, EventArgs e)
{
    // Get the words from the e-book.
    string[] words = theEBook.Split(
        new char[] { ' ', '\u000A', ',', '.', ';', ':', '-', '?', '/' },
        StringSplitOptions.RemoveEmptyEntries);
    string[] tenMostCommon = null;
    string longestWord = string.Empty;

    Parallel.Invoke(
        () =>
        {
            // Now, find the ten most common words.
            tenMostCommon = FindTenMostCommon(words);
        },
        () =>
        {
            // Get the longest word.
            longestWord = FindLongestWord(words);
        });

    // Now that all tasks are complete, build a string to show all
    // stats in a message box.
...
}

The Parallel.Invoke() method expects a parameter array of Action<> delegates, which you have supplied indirectly using lambda expressions. Again, while the output is identical, the benefit is that the TPL will now make use of all possible processors on the machine to invoke each method in parallel if possible.

Source Code The MyEBookReader project is included under the Chapter 19 subdirectory.